package com.tools.middle.redis;

import com.tools.common.exception.InvalidOperationException;
import com.tools.common.object.Note;
import com.tools.common.object.string.VerifyKit;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.ClusterServersConfig;
import org.redisson.config.Config;
import org.redisson.config.SentinelServersConfig;
import org.redisson.config.SingleServerConfig;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.*;

import java.time.Duration;
import java.util.LinkedHashSet;

/**
 * Redis 客户端实例的构建工厂工具类
 * */
@Note("Redis 客户端实例的构建工厂工具类")
public final class RedisKit {

    private RedisKit() {}

    /* **********************************************************************************
     *
     *          Jedis 连接池/ Jedis 集群配置
     *
     * *********************************************************************************
     * */

    @Note("【单节点连接池配置对象】：根据实体的配置项构建一个 Redis 的连接池配置对象 JedisPoolConfig")
    public static JedisPoolConfig newJedisPoolConfig(RedisSource source) {
        if(source == null) throw new NullPointerException("Redis 配置项包装实体为 null");
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(source.getMaxTotal());
        jedisPoolConfig.setMaxIdle(source.getMaxIdle());
        jedisPoolConfig.setMinIdle(source.getMinIdle());
        jedisPoolConfig.setMaxWait(Duration.ofMillis(source.getMaxWait()));
        return jedisPoolConfig;
    }

    @Note("根据配置和节点的主机、端口构建一个 Jedis 连接池实例")
    public static JedisPool newJedisPool(RedisSource source) {
        JedisPoolConfig config = newJedisPoolConfig(source);
        HostAndPort node = source.firstNode();
        int socketTimeout = source.getSocketTimeout();
        String clientName = source.getClientName();
        if(source.usernameNotEmpty()) {
            String username = source.getUsername();
            String password = source.getPassword();
            int databaseIndex = source.getDatabaseIndex();
            int connectionTimeout = source.getConnectionTimeout();
            return new JedisPool(config, node.getHost(), node.getPort(), connectionTimeout, socketTimeout, username, password, databaseIndex, clientName);
        }
        if(source.passwordNotEmpty()) {
            String password = source.getPassword();
            int databaseIndex = source.getDatabaseIndex();
            int connectionTimeout = source.getConnectionTimeout();
            return new JedisPool(config, node.getHost(), node.getPort(), connectionTimeout, socketTimeout, password, databaseIndex, clientName);
        }
        return new JedisPool(config, node.getHost(), node.getPort(), socketTimeout, clientName);
    }


    @Note("根据配置和节点的主机、端口构建一个 JedisCluster 集群客户端实例")
    public static JedisCluster newJedisCluster(RedisSource source) {
        JedisPoolConfig config = newJedisPoolConfig(source);
        LinkedHashSet<HostAndPort> hostAndPortSet = source.getHostAndPortSet();
        int socketTimeout = source.getSocketTimeout();
        int connectionTimeout = source.getConnectionTimeout();
        int maxAttempts = source.getMaxAttempts();
        if(source.usernameNotEmpty()) {
            String username = source.getUsername();
            String password = source.getPassword();
            String clientName = source.getClientName();
            return new JedisCluster(hostAndPortSet, connectionTimeout, socketTimeout, maxAttempts, username, password, clientName, config);
        }
        if(source.passwordNotEmpty()) {
            String password = source.getPassword();
            String clientName = source.getClientName();
            return new JedisCluster(hostAndPortSet, connectionTimeout, socketTimeout, maxAttempts, password, clientName, config);
        }
        return new JedisCluster(hostAndPortSet, connectionTimeout, socketTimeout, maxAttempts, config);
    }


    @Note("根据配置和节点的主机、端口、主节点名称构建一个 JedisSentinelPool 哨兵集群客户端连接池实例")
    public static JedisSentinelPool newJedisSentinelPool(RedisSource source) {
        if(source == null || !source.isSentinel()) throw new IllegalArgumentException("构建哨兵集群客户端所需配置项不能为 null 且主节点名称不能为空");
        JedisPoolConfig config = newJedisPoolConfig(source);
        LinkedHashSet<String> hosts = private_hostAndPortSetToStringSet(source.getHostAndPortSet());
        int socketTimeout = source.getSocketTimeout();
        int connectionTimeout = source.getConnectionTimeout();
        String masterName = source.getMasterName();
        int databaseIndex = source.getDatabaseIndex();
        if(source.usernameNotEmpty()) {
            String username = source.getUsername();
            String password = source.getPassword();
            String clientName = source.getClientName();
            return new JedisSentinelPool(masterName, hosts, config, connectionTimeout, socketTimeout, username, password,databaseIndex, clientName);
        }
        if(source.passwordNotEmpty()) {
            String password = source.getPassword();
            String clientName = source.getClientName();
            return new JedisSentinelPool(masterName, hosts, config, connectionTimeout, socketTimeout, password, databaseIndex, clientName);
        }
        return new JedisSentinelPool(masterName, hosts, config, connectionTimeout);
    }

    /* **********************************************************************************
     *
     *          其他方法
     *
     * *********************************************************************************
     * */

    @Note("将 【IP:端口】 的字符串构建为 HostAndPort 实例对象")
    public static HostAndPort newHostAndPort(String hostAndPort) {
        if(VerifyKit.isHostAndPort(hostAndPort)) {
            String[] array = hostAndPort.split(":");
            int port = Integer.parseInt(array[1]);
            return new HostAndPort(array[0], port);
        }
        throw new IllegalArgumentException("Redis 节点 【" + hostAndPort + "】 不是标准的 【ip:端口号】 格式字符串");
    }

    @Note("将 IP/主机 和端口构建为 HostAndPort 实例对象")
    public static HostAndPort toHostAndPort(String host, int port) {
        String hostAndPorts = host + ":" + port;
        if(VerifyKit.isHostAndPort(hostAndPorts)) {
            return new HostAndPort(host, port);
        }
        throw new IllegalArgumentException("Redis 节点 【" + hostAndPorts + "】 不是标准的 【ip:端口号】 格式字符串");
    }


    /* **********************************************************************************
     *
     *          Spring-Data-Redis 的方法
     *
     * *********************************************************************************
     * */

    @Note("返回指定泛型的 RedisTemplate 对象。RedisConnectionFactory 对象可以从 Spring 容器中获取")
    public static <K, V, HK, HV> RedisTemplate<K, V> newRedisTemplate(RedisConnectionFactory factory,
                                                              RedisSerializer<K> keySerializer, RedisSerializer<V> valueSerializer,
                                                              RedisSerializer<HK> hashKeySerializer, RedisSerializer<HV> hashValueSerializer) {
        RedisTemplate<K, V> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        redisTemplate.setKeySerializer(keySerializer);
        redisTemplate.setHashKeySerializer(hashKeySerializer);
        redisTemplate.setValueSerializer(valueSerializer);
        redisTemplate.setHashValueSerializer(hashValueSerializer);
        return redisTemplate;
    }


    @Note("返回 key-value 都是字符串类型的 RedisTemplate 对象。RedisConnectionFactory 对象可以从 Spring 容器中获取")
    public static RedisTemplate<String, String> newStringRedisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        redisTemplate.setValueSerializer(valueSerializer);
        redisTemplate.setHashValueSerializer(valueSerializer);
        return redisTemplate;
    }

    @Note("返回标准类型的 RedisTemplate 对象。RedisConnectionFactory 对象可以从 Spring 容器中获取")
    public static RedisTemplate<String, Object> newStandardRedisTemplate(RedisConnectionFactory factory) {
        StringRedisSerializer keySerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
        return newRedisTemplate(factory, keySerializer, valueSerializer, keySerializer, valueSerializer);
    }

    @Note("从 RedisConnectionFactory Redis 连接工厂中获取 Redis 节点连接对象")
    public static RedisConnection newRedisConnection(RedisConnectionFactory factory) {
        return factory.getConnection();
    }


    /* **********************************************************************************
     *
     *         Redisson 的方法
     *
     * *********************************************************************************
     * */

    @Note("生成 Redisson 客户端")
    public static RedissonClient newRedissonClient(RedisSource source) {
        if(source == null) throw new NullPointerException("Redis 的配置项为空");
        Config config = new Config();
        if(source.isSingle()) {
            HostAndPort first = source.getFirst();
            SingleServerConfig singleServerConfig = config.useSingleServer()
                    .setAddress("redis://" + first.getHost() + ":" + first.getPort())
                    .setDatabase(source.getDatabaseIndex())
                    .setConnectionPoolSize(source.getMaxTotal())
                    .setConnectionMinimumIdleSize(source.getMinIdle())
                    .setIdleConnectionTimeout(source.getMaxWait())
                    .setConnectTimeout(source.getConnectionTimeout())
                    .setTimeout(source.getSocketTimeout())
                    .setRetryAttempts(source.getMaxAttempts());
            if(source.usernameNotEmpty()) singleServerConfig.setUsername(source.getUsername());
            if(source.passwordNotEmpty()) singleServerConfig.setPassword(source.getPassword());
            return Redisson.create(config);
        }
        if(source.isCluster()) {
            String[] hosts = private_hostAndPortSetToStringArray(source.getHostAndPortSet());
            ClusterServersConfig clusterServersConfig = config.useClusterServers()
                    .addNodeAddress(hosts)
                    .setMasterConnectionPoolSize(source.getMaxTotal())
                    .setSlaveConnectionPoolSize(source.getMaxTotal())
                    .setMasterConnectionMinimumIdleSize(source.getMinIdle())
                    .setSlaveConnectionMinimumIdleSize(source.getMinIdle())
                    .setIdleConnectionTimeout(source.getMaxWait())
                    .setConnectTimeout(source.getConnectionTimeout())
                    .setTimeout(source.getSocketTimeout())
                    .setRetryAttempts(source.getMaxAttempts());
            if(source.usernameNotEmpty()) clusterServersConfig.setUsername(source.getUsername());
            if(source.passwordNotEmpty()) clusterServersConfig.setPassword(source.getPassword());
            return Redisson.create(config);
        }
        if(source.isSentinel()) {
            String[] hosts = private_hostAndPortSetToStringArray(source.getHostAndPortSet());
            SentinelServersConfig sentinelServersConfig = config.useSentinelServers()
                    .setMasterName(source.getMasterName())
                    .addSentinelAddress(hosts)
                    .setMasterConnectionPoolSize(source.getMaxTotal())
                    .setMasterConnectionMinimumIdleSize(source.getMinIdle())
                    .setSlaveConnectionPoolSize(source.getMaxTotal())
                    .setSlaveConnectionMinimumIdleSize(source.getMinIdle())
                    .setIdleConnectionTimeout(source.getMaxWait())
                    .setConnectTimeout(source.getConnectionTimeout())
                    .setTimeout(source.getSocketTimeout())
                    .setRetryAttempts(source.getMaxAttempts());
            if(source.usernameNotEmpty()) sentinelServersConfig.setUsername(source.getUsername());
            if(source.passwordNotEmpty()) sentinelServersConfig.setPassword(source.getPassword());
            return Redisson.create(config);
        }
        throw new InvalidOperationException("目前仅支持构建 “单机、集群、哨兵” 三个客户端的操作");
    }


    /* **********************************************************************************
     *
     *         私有逻辑
     *
     * *********************************************************************************
     * */

    @Note("将 hostAndPortSet 集合转为符合 redis 协议的 “主机:端口” 字符串数组")
    private static String[] private_hostAndPortSetToStringArray(LinkedHashSet<HostAndPort> hostAndPortSet) {
        String[] hosts = new String[hostAndPortSet.size()];
        int i = 0;
        for(HostAndPort hostAndPort : hostAndPortSet) {
            hosts[i] = "redis://" + hostAndPort.getHost() + ":" + hostAndPort.getPort();
            i++;
        }
        return hosts;
    }


    @Note("将 hostAndPortSet 集合转为符合 “主机:端口” 格式的字符串集合")
    private static LinkedHashSet<String> private_hostAndPortSetToStringSet(LinkedHashSet<HostAndPort> hostAndPortSet) {
        LinkedHashSet<String> hosts = new LinkedHashSet<>((int) ((hostAndPortSet.size() / 0.75) + 1));
        for(HostAndPort hostAndPort : hostAndPortSet) {
            hosts.add(hostAndPort.toString());
        }
        return hosts;
    }
}
